Как документировать REST API с помощью Swagger в Spring Boot 3

Как документировать REST API с помощью Swagger в Spring Boot 3

Документирование RESTful API (Application Programming Interface) — неотъемлемая часть создания веб-приложений. На смену ручному документированию API пришли инструменты, которые решают эту задачу быстрее.

Из этой статьи вы узнаете, как внедрить Swagger, основанный на спецификации OpenAPI 3.0, в проект на Spring Boot.

Содержание:

Что такое Swagger?

Swagger — набор инструментов для создания, редактирования, кодогенерации и использования API-документации в соответствии со спецификацией OpenAPI. С появлением Swagger отпала необходимость писать длинную API-документацию вручную. Swagger умеет читать структуру API, определённую с помощью аннотаций в коде, и генерировать из неё спецификацию API.

У Swagger также есть UI, который показывает интерактивную API-документацию, как в примере Petstore. Этот UI также позволяет тестировать API, выполняя запросы в браузере.

Ключевые компоненты Swagger:

  • Swagger Editor для написания и редактирования API-спецификации,
  • Swagger UI для создания интерактивной API-документации,
  • Swagger Codegen для генерации серверного макета или клиентской библиотеки для вашего API.

В контексте API-документации термины Swagger и OpenAPI часто идут вместе, но это не синонимы. OpenAPI — спецификация для описания API, а Swagger — фреймворк для создания API-документации в соответствии с этой спецификацией.

Внедрение Swagger в проект на Spring Boot

После прочтения этого раздела вы создадите своё небольшое CRUD-приложение, сгенерируете к нему API-документацию и увидите её в Swagger UI.

Предварительные требования:

  • JDK 17 или новее (мы используем Axiom JDK),
  • Maven,
  • Docker,
  • любая IDE.

1. Создайте проект REST API

Если у вас уже есть готовый проект, то пропустите этот шаг. Вы можете идти по руководству или использовать его отдельные примеры в своём проекте.

Код демо-проекта доступен на GitHub.

У нашего демо-приложения на Spring Boot будет REST API для управления сотрудниками. У сотрудника есть:

  • идентификационный номер (id),
  • имя (firstName),
  • фамилия (lastName).

Вот таблица с методами API, которые мы реализуем:

МетодЭндпоинтОписание
GET/v1/employeesПолучить список всех сотрудников
GET/v1/employees/{employeeID}Получить данные о конкретном сотруднике
POST/v1/employeesДобавить сотрудника
PUT/v1/employees/Обновить данные о сотруднике
DELETE/v1/employees/{employeeID}Удалить сотрудника

Чтобы сэкономить время и силы и не создавать локальную БД, мы будем использовать Testcontainers, которые обеспечивают подключение к готовым образам баз данных в Docker.

Чтобы создать основу проекта, сделайте следующее:

1) Откройте Spring Initializr.

  1. В разделе Language выберите Java,
  2. ProjectMaven,
  3. Project Metadata -> Artifact — openapidemo (или любое другое имя на ваш выбор),
  4. Project Metadata -> Name — имя проекта (у нас это OpenapiSpringDemo),
  5. PackagingJar,
  6. Java17 или ту, с которой вы работаете.

В некоторых IDE, например в IntelliJ IDEA Ultimate, Spring Initializr доступен в виде плагина. Это значит, что вам не нужно скачивать архив проекта, распаковывать его и импортировать в IDE. Достаточно просто настроить всё, что нужно, не выходя из IDE.

2) Добавьте следующие зависимости, нажав ADD DEPENDENCIES…

  1. Spring Web,
  2. Lombok,
  3. Spring Data JDBC,
  4. PostgreSQL Driver,
  5. Testcontainers,
  6. Jakarta Persistence API,
  7. Spring Boot DevTools. DevTools экономит время привнесении изменений в коде. Вам не нужно перезапускатьприложение каждый раз, когда вы вносите изменения,перекомпиляция выполняется “на лету”.

Добавьте необходимые зависимости

3) Сгенерируйте проект, нажав GENERATE. У вас скачается файл demo.zip. Разархивируйте его и импортируйте проект в свою IDE.

Структура проекта будет простая:

  • Класс Employee, который будет предоставлять информацию о сотрудниках.
  • Контроллер EmployeeController, который будет предоставлять RESTful API для работы с объектами класса Employee.
  • Интерфейс EmployeeRepository для работы с объектами класса Employee и выполнения базовых операций CRUD (Create, Read, Update, Delete).
  • Класс EmployeeNotFoundException для обработки ошибок.

Employee.java

package com.example.openapidemo;

import jakarta.persistence.GenerationType;
import jakarta.validation.constraints.NotNull;
import jakarta.persistence.GeneratedValue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.Id;

@Getter
@Setter
@RequiredArgsConstructor
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @NotNull
    private String firstName;

    @NotNull
    private String lastName;

}

EmployeeRepository.java

package com.example.openapidemo;

import org.springframework.data.repository.ListCrudRepository;

public interface EmployeeRepository extends ListCrudRepository<Employee, Integer> { }

EmployeeController.java

package com.example.openapidemo;


import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;

import java.util.List;

@RestController
@AllArgsConstructor
@RequestMapping("/v1")
public class EmployeeController {
    
    private EmployeeRepository repository;

    @GetMapping("/employees")
    public List<Employee> findAllEmployees() {
        return repository.findAll();
    }

    @GetMapping("/employees/{employeeId}")
    public Employee getEmployee(@PathVariable int employeeId) {
        return repository.findById(employeeId)
                .orElseThrow(() -> new EmployeeNotFoundException(employeeId));
    }

    @PostMapping("/employees")
    public ResponseEntity<Employee> addEmployee(@RequestBody Employee newEmployee) {
        Employee savedEmployee = repository.save(newEmployee);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedEmployee);
    }

    @PutMapping("/employees/")
    public ResponseEntity<Employee> updateEmployee(@RequestBody Employee employee) {
        return repository.findById(employee.getId())
                .map(existingEmployee -> {
                    existingEmployee.setFirstName(employee.getFirstName());
                    existingEmployee.setLastName(employee.getLastName());
                    Employee updatedEmployee = repository.save(existingEmployee);
                    return ResponseEntity.ok(updatedEmployee);
                })
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @DeleteMapping("/employees/{employeeId}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public ResponseEntity<Void> deleteEmployee(@PathVariable int employeeId) {
        repository.findById(employeeId).ifPresentOrElse(
                employee -> repository.deleteById(employeeId),
                () -> {
                    throw new EmployeeNotFoundException(employeeId);
                }
        );
        return ResponseEntity.noContent().build();
    }

}

EmployeeNotFoundException.java

package com.example.openapidemo;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class EmployeeNotFoundException extends RuntimeException {
    public EmployeeNotFoundException (int employeeId) {
        super("Сотрудник с ID " + employeeId + " не найден");
    }
}

4) В файл pom.xml добавьте зависимость для Jakarta Persistence API:

<dependency>
	<groupId>jakarta.persistence</groupId>
	<artifactId>jakarta.persistence-api</artifactId>
</dependency>

5) В папке test откройте класс TestOpenapiSpringDemoApplication (имя вашего класса может отличаться). Добавьте аннотацию @RestartScope в метод PostgreSQLContainer<?> postgresContainer(). Так сохранится бин. БД не будет перезапускаться каждый раз с перезапуском приложения.

6) Предоставьте схему базы данных. Для этого в директории resources создайте файл schema.sql со следующим содержимым:

CREATE TABLE IF NOT EXISTS employee (
    id           SERIAL PRIMARY KEY,
    first_name   VARCHAR(255) NOT NULL,
    last_name    VARCHAR(255) NOT NULL
);

7) (Необязательно) Добавьте в БД несколько записей, чтобы не видеть страницу ошибки в браузере при запуске приложения. Для разработки API это не нужно. В той же директории resources создайте файл data.sql:

TRUNCATE employee;
INSERT INTO employee (first_name, last_name)
VALUES ('Иван', 'Иванов'),
       ('Петр', 'Петров');

8) Установите режим инициализации SQL, добавив в файл aplication.properties следующую строку:

spring.sql.init.mode=always

Это нужно, потому что мы инициализируем БД с помощью SQL-скрипта.

9) Проверьте, что всё работает как ожидается. Для этого запустите на своей машине Docker и запустите класс TestOpenapiSpringDemoApplication. Spring Boot подтянет образ базы данных PostgreSQL за вас.

В браузере откройте http://localhost:8080/v1/employees. Вы должны увидеть что-то подобное:

[
  {
    "id": 1,
    "firstName": "Иван",
    "lastName": "Иванов"
  },
  {
    "id": 2,
    "firstName": "Петр",
    "lastName": "Петров"
  }
]

Простое CRUD-приложение готово для экспериментов.

2. Добавьте зависимость для springdoc-openapi

Для работы с Swagger вам нужна библиотека springdoc-api. Эта библиотека нужна для генерации API-документации, совместимой с спецификацией OpenAPI, для проектов на Spring Boot. springdoc-api поддерживает Swagger UI и другие инструменты, такие как OAuth2 и GraalVM Native Image.

В файл pom.xml добавьте зависимость для springdoc-api:

<dependency>
   <groupId>org.springdoc</groupId>
   <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
   <version>2.6.0</version>
</dependency>

Никаких дополнительных настроек не требуется.

3. Сгенерируйте API-документацию

Документация OpenAPI генерируется при сборке проекта. Убедитесь, что всё работает корректно. Для этого запустите ваш проект и перейдите на страницу API-документации по умолчанию: http://localhost:8080/v3/api-docs.

Чтобы выложить документацию в продакшен, замените localhost:8080 на публичный адрес.

Вы увидите эндпоинты (endpoints) и данные по ним в формате JSON:

{
  "openapi": "3.0.1",
  "info": {
    "title": "OpenAPI definition",
    "version": "1.0"
  },
  "servers": [
    {
      "url": "http://localhost:8080",
      "description": "Generated server url" 
    }
  ],
  "paths": {
    "/employees": {
      "get": {
        "tags": [
          "employee-controller"
        ],
        "operationId": "findAllEmployees",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "*/*": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Employee"
                  }
                }
              }
            }
          }
        }
      },
...

Вы также можете открыть YAML-документ с теми же данными по следующей ссылке: http://localhost:8080/v3/api-docs.yaml.

При необходимости вы можете изменить путь по умолчанию до API-документации в файле application.properties. Например:

springdoc.api-docs.path=/api-docs

Теперь документация доступна по ссылке: http://localhost:8080/api-docs.

4. Откройте Swagger UI

Поскольку библиотека springdoc-openapi уже поддерживает Swagger UI, вам не нужно настраивать этот инструмент отдельно.

По ссылке http://localhost:8080/swagger-ui/index.html вы увидите интерфейс для взаимодействия с эндпоинтами:

Интерфейс для взаимодействия с эндпоинтами

Все эндпоинты можно раскрыть и посмотреть как будет выглядеть API-запрос и ответ на него. Для этого нажмите Try it out и Execute.

Как будет выглядеть API-запрос и ответ на него

Расширенная настройка Swagger в Spring Boot 3 с помощью аннотаций

Текущая API-документация не сильно информативна. Её можно дополнить с помощью аннотаций в коде приложения. Ниже приведены самые популярные аннотации. Список всех аннотаций можно посмотреть в вики Swagger.

Добавление описания Swagger API

Добавьте в API-документацию имя и контакты автора проекта, а также описание, для чего нужен этот API. Для этого создайте класс OpenAPIConfiguration:

Из соображений безопасности приложения переопределите информацию о URL сервера и номере порта в application.properties: api.server.url=http://localhost:8080

package com.example.openapidemo;

import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import java.util.List;

@Configuration
@AllArgsConstructor
public class OpenAPIConfiguration {

    private Environment environment;

    @Bean
    public OpenAPI defineOpenAPI () {
        Server server = new Server();
        String serverUrl = environment.getProperty("api.server.url");
        server.setUrl(serverUrl);
        server.setDescription("Development");

        Contact myContact = new Contact();
        myContact.setName("Имя Фамилия");
        myContact.setEmail("my.email@example.com");

        Info info = new Info()
                .title("Системное API для управления сотрудниками")
                .version("1.0")
                .description("Это API предоставляет эндпоинты для управления сотрудниками.")
                .contact(myContact);
        return new OpenAPI().info(info).servers(List.of(server));
    }
}

Вы также можете добавить информацию о лицензии и др. Кода выше достаточно для того, чтобы посмотреть, как это будет выглядеть в документации. Запустите проект, откройте страницу с API-документацией по ссылке http://localhost:8080/swagger-ui/index.html и убедитесь, что введённые вами данные отображаются:

как это будет выглядеть в документации

Валидация бина (Bean)

Библиотека springdoc-openapi поддерживает стандарт JSR 303: Bean Validation (например, @NotNull, @Min, @Max и @Size) для валидации входных данных. Добавьте эти аннотации в код проекта, и у вас автоматически сгенерируется схема в разделе Schemas Swagger UI.

Определите аннотации в классе Employee:

package com.example.openapidemo;

import jakarta.persistence.GenerationType;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import jakarta.persistence.GeneratedValue;

@Getter
@Setter
@RequiredArgsConstructor
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @NotNull
    @Size(min = 1, max = 50)
    private String firstName;

    @NotNull
    @Size(min = 1, max = 70)
    private String lastName;

}

Перекомпилируйте проект и откройте документацию. Вы увидите, что раздел Schemas обновился:

Schemas

@Tag

С помощью @Tag можно группировать классы и методы API.

Вот как можно использовать эту аннотацию для группировки GET-методов в файле контроллера EmployeeController:

import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = "get", description = "GET-методы Employee API")
@GetMapping("/employees")
public List<Employee> findAllEmployees() {
    return repository.findAll();
}

@Tag(name = "get", description = "GET-методы Employee API")
@GetMapping("/employees/{employeeId}")
public Employee getEmployee(@Parameter(
        description = "ID сотрудника, данные по которому запрашиваются",
        required = true)
                            @PathVariable int employeeId) {
    return repository.findById(employeeId)
            .orElseThrow(() -> new EmployeeNotFoundException(employeeId));
    }

Когда вы откроете документацию, то увидите, что группировка методов изменилась: GET-методы теперь сгруппированы отдельно от остальных.

GET-методы

@Operation

С помощью @Operation можно добавить краткое описание (summary) и расширенное описание (description) метода API.

Рассмотрим применение @Operation для описания метода updateEmployee() в том же файле контроллера EmployeeController:

import io.swagger.v3.oas.annotations.Operation;

@Operation(summary = "Обновить данные о сотруднике", description = "В ответе возвращается объект Employee c полями id, firstName и lastName.")
@PutMapping("/employees/")
public ResponseEntity<Employee> updateEmployee(@RequestBody Employee employee) {
    return repository.findById(employee.getId())
            .map(existingEmployee -> {
                existingEmployee.setFirstName(employee.getFirstName());
                existingEmployee.setLastName(employee.getLastName());
                Employee updatedEmployee = repository.save(existingEmployee);
                return ResponseEntity.ok(updatedEmployee);
                })
            .orElseGet(() -> ResponseEntity.notFound().build());
}

Описание API-метода в Swagger UI будет выглядеть так:

Описание API-метода

@ApiResponses

@ApiResponses даёт описание ответа для метода. Каждый ответ помечается тегом @ApiResponse:

import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;

@ApiResponses({
        @ApiResponse(responseCode = "204", description = "Сотрудник успешно удалён"),
        @ApiResponse(responseCode = "404", description = "Сотрудник не найден")
})
@DeleteMapping("/employees/{employeeId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public ResponseEntity<Void> deleteEmployee(@PathVariable int employeeId) {
    repository.findById(employeeId).ifPresentOrElse(
            employee -> repository.deleteById(employeeId),
            () -> {
                throw new EmployeeNotFoundException(employeeId);
            }
    );
    return ResponseEntity.noContent().build();
}

После перекомпиляции класса EmployeeController, описание API-ответа обновится:

EmployeeController

@Parameter

Аннотация @Parameter используется для параметра API-метода и описывает этот параметр операции.

Рассмотрим на примере метода getEmployee():

import io.swagger.v3.oas.annotations.Parameter;

@GetMapping("/employees/{employeeId}")
public Employee getEmployee(@Parameter(
        description = "ID сотрудника, данные по которому запрашиваются",
        required = true)
                            @PathVariable int employeeId) {
    return repository.findById(employeeId)
            .orElseThrow(() -> new EmployeeNotFoundException(employeeId));
}

Атрибут description даёт дополнительную информацию о назначении параметра, а required = true показывает, что параметр обязателен для заполнения.

В Swagger UI вы увидите следующее:

Swagger UI

Заключение

Документирование API c помощью Swagger удобно. Такое решение позволяет генерировать полную и единообразную документацию к API без ручного труда. Это ускоряет разработку и сводит к минимуму риски ошибок.

Приложения на Spring Boot разворачиваются в контейнерах. Для этого отлично подходит Axiom Runtime Container Pro. Это такая же хорошая практика, как и документирование API.

Author image

Сергей Лунегов

Директор по продуктам Axiom JDK

Axiom JDK info@axiomjdk.ru Axiom JDK logo Axiom Committed to Freedom 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67 Axiom JDK 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67